Skip to content

fix(solver): reject non-finite input traces at the FFI boundaries#161

Merged
daharoni merged 1 commit into
mainfrom
fix/ffi-nonfinite-guard
Jul 8, 2026
Merged

fix(solver): reject non-finite input traces at the FFI boundaries#161
daharoni merged 1 commit into
mainfrom
fix/ffi-nonfinite-guard

Conversation

@daharoni

@daharoni daharoni commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

Neither FFI boundary validated trace finiteness. A NaN/Inf in an input trace propagated silently:

  • total_cmp sorts NaN last, corrupting the rolling-baseline percentile pick;
  • alpha / PVE come back NaN, and the result is returned with converged=falseindistinguishable from a legitimately hard trace;
  • in the batch path a NaN actually panicked deep in the FFT (Result::unwrap on a non-real spectrum), aborting the whole call.

This is a pre-publication defensibility gap: silent garbage results.

Fix

Add a shared crate::first_nonfinite(&[f32]) -> Option<usize> helper and guard every FFI trace entry, returning a clear typed error (fail loud — chosen over sanitize/skip):

PyO3 (py_api.rs)

  • to_f32_vec — covers deconvolve_single, py_indeca_solve_trace, py_seed_trace, py_indeca_estimate_kernel, py_indeca_fit_biexponential
  • PySolver::set_trace
  • deconvolve_batch — inline 2D row conversion bypasses to_f32_vec
  • seed_kernel_estimate — inline 2D flat-buffer build

WASM (js_indeca.rs)indeca_solve_trace and seed_trace now return Result<JsValue, JsError> and throw on non-finite input. Both CaDecon worker handlers already wrap these in try/catch, so the error surfaces as a job error (no silent garbage, no hang).

The shared Solver core signature is unchanged (native/wasm/pyo3 all compile); validation lives at the entry boundaries.

Tests

  • Rust: first_nonfinite unit tests (clean / NaN / ±Inf).
  • Python: test_input_validation.py covers single + batch run_deconvolution, seed_kernel_estimate, and PySolver.set_trace.
  • The batch and seed_kernel_estimate gaps were caught by these tests, not by the 1D guard alone — both inline 2D paths bypassed to_f32_vec.

All suites green: 128 Rust, 6 Python validation, 289 TS; cargo fmt/clippy clean on both feature sets; WASM builds; typecheck clean.

Follow-up (noted, not in this PR)

CaTune calls Solver.set_trace directly in the browser — guarding that path needs a shared-Solver-signature change (it's used by native/pyo3/wasm/tests), so it's deferred to a CaTune-scoped change. CaDecon (the app under review) and all of Python are fully covered here.

Part of the config/numerical-hygiene series (item: FFI NaN guard). Next: surface the biexp/degenerate fallback status.

🤖 Generated with Claude Code

A NaN/Inf in an input trace previously propagated silently: total_cmp sorts
NaN last (corrupting the rolling-baseline percentile), alpha/PVE come back NaN,
and the result is returned with converged=false — indistinguishable from a
legitimately hard trace. In the batch path a NaN actually panicked deep in the
FFT (Result::unwrap on a non-real spectrum), aborting the call.

Add a shared crate::first_nonfinite helper and guard every FFI trace entry,
returning a clear typed error (fail loud, no garbage):

PyO3 (py_api.rs):
- to_f32_vec (covers deconvolve_single, py_indeca_solve_trace, py_seed_trace,
  py_indeca_estimate_kernel, py_indeca_fit_biexponential)
- PySolver::set_trace
- deconvolve_batch (inline 2D row conversion bypasses to_f32_vec)
- seed_kernel_estimate (inline 2D flat-buffer build)

WASM (js_indeca.rs): indeca_solve_trace and seed_trace now return
Result<JsValue, JsError> and throw on non-finite input. Both CaDecon worker
handlers already wrap these in try/catch, so the error surfaces as a job error.

The shared Solver core signature is unchanged (native/wasm/pyo3 all compile);
validation lives at the entry boundaries. CaTune uses Solver.set_trace directly
in the browser — noted as a follow-up (needs a shared-signature change).

Tests: Rust first_nonfinite unit tests; Python test_input_validation covers
single/batch run_deconvolution, seed_kernel_estimate, and PySolver.set_trace.
The batch and seed_kernel_estimate gaps were caught by these tests, not by the
1D guard alone. All suites green (128 Rust, 6 Python, 289 TS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daharoni daharoni merged commit 08c7504 into main Jul 8, 2026
6 checks passed
@daharoni daharoni deleted the fix/ffi-nonfinite-guard branch July 8, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant